18.2 初始化

调度器初始化函数schedinit在前面已多次提及,除去内存分配、垃圾回收等操作外,针对自身的初始化无非是MaxMcount、GOMAXPROCS。

proc1.go

func schedinit() { // 设置最大M数量 sched.maxmcount=10000

// 初始化栈空间复用管理链表 stackinit()

// 初始化当前M mcommoninit(g.m)

// 默认值总算从1调整为CPU Core数量了 procs:=int(ncpu) if n:=atoi(gogetenv(“GOMAXPROCS”));n>0{ if n> _MaxGomaxprocs{ n= _MaxGomaxprocs } procs=n }

// 调整P数量 // 注意:此刻所有P都是新建的,所以不可能返回有本地任务的P if procresize(int32(procs)) !=nil{ throw(“unknown runnable goroutine during bootstrap”) } }

GOMAXPROCS默认值总算从1改为CPU Cores了。

因为P的数量有最大限制,所以用一个足够大的数组存储才是最正确的做法。虽然浪费点空间,但省去很多内存增减的麻烦。

runtime2.go

var allp[_MaxGomaxprocs+1]*p

type schedt struct{ pidle puintptr// 空闲P链表 npidle uint32 // 空闲P数量 }

调整P的数量并不意味着全部分配新对象,仅仅做去余补缺即可。

proc1.go

func procresize(nprocs int32) *p { old := gomaxprocs

// 新增 for i := int32(0); i < nprocs; i++ { pp := allp[i]

 // 申请新 P 对象 
 if pp == nil { 
     pp = new(p) 
     pp.id = i 
     pp.status = _Pgcstop 

     // 保存到 allp 
     atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp)) 
 } 

 // 为 P 分配 cache 对象 
 if pp.mcache == nil { 
     if old == 0 && i == 0 { 
         // bootstrap 
         pp.mcache = getg().m.mcache 
     } else { 
         // 创建 cache 
         pp.mcache = allocmcache() 
     } 
 } 

}

// 释放多余的 P for i := nprocs; i < old; i++ { p := allp[i]

 // 将本地任务转移到全局队列 
 for p.runqhead != p.runqtail { 
     p.runqtail-- 
     gp := p.runq[p.runqtail%uint32(len(p.runq))] 
     globrunqputhead(gp) 
 } 
 if p.runnext != 0 { 
     globrunqputhead(p.runnext.ptr()) 
     p.runnext = 0 
 } 

 // 释放当前 P 绑定的 cache 
 freemcache(p.mcache) 
 p.mcache = nil 

 // 将当前 P 的 G 复用链转移到全局 
 gfpurge(p) 

 // 似乎就丢在那里不管了,反正也没剩下啥 
 p.status = _Pdead 
 // can't free P itself because it can be referenced by an M in syscall 

}

g := getg()

// 如果当前正在用的 P 属于被释放的那拨,那就换成 allp[0] // 调度器初始化阶段,根本没有 P,那就绑定 allp[0] if g.m.p != 0 && g.m.p.ptr().id < nprocs { // 继续使用当前 P g.m.p.ptr().status = _Prunning } else { // 释放当前 P,因为它已经失效 if g.m.p != 0 { g.m.p.ptr().m = 0 } g.m.p = 0 g.m.mcache = nil

 // 换成 allp[0] 
 p := allp[0] 
 p.m = 0 
 p.status = _Pidle 
 acquirep(p) 

}

// 将没有本地任务的 P 放到空闲链表 var runnablePs *p for i := nprocs - 1; i >= 0; i— { p := allp[i]

 // 确保不是当前正在用的 P 
 if _g_.m.p.ptr() == p { 
     continue 
 } 

 p.status = _Pidle 
 if runqempty(p) { 
     // 放入空闲链表 
     pidleput(p) 
 } else { 
     // 有本地任务,构建链表 
     p.m.set(mget()) 
     p.link.set(runnablePs) 
     runnablePs = p 
 } 

}

// 返回有本地任务的 P(链表) return runnablePs }

// 将 P 放入空闲链表 func pidleput(p *p) { p.link = sched.pidle sched.pidle.set(p) xadd(&sched.npidle, 1) }

默认只有schedinit和startTheWorld会调用procresize函数。在调度器初始化阶段,所有P对象都是新建的。除分配给当前主线程的外,其他都被放入空闲链表。而startTheWorld会激活全部有本地任务的P对象(详见后文)。

在完成调度器初始化后,引导过程才创建并运行main goroutine。

asm_amd64.s

TEXT runtime·rt0_go(SB),NOSPLIT,$0 // save mg0 = g0 MOVQ CX, m_g0(AX) // save m0 to g0m MOVQ AX, g_m(CX)

CALL runtime·schedinit(SB)

// 创建 main goroutine,并将其放入当前 P 本地队列 MOVQ 0
CALL runtime·newproc(SB) POPQ AX POPQ AX

// 让当前 M0 进入调度,执行 main goroutine CALL runtime·mstart(SB)

// M0 永远不会执行这条崩溃测试指令 MOVL $0xf1, 0xf1 // crash RET

虽然可在运行期用runtime.GOMAXPROCS函数修改P的数量,但须付出极大代价。

debug.go

func GOMAXPROCS(n int) int { if n > _MaxGomaxprocs { n = _MaxGomaxprocs }

// 返回当前值(这个才是最常用的做法) ret := int(gomaxprocs) if n 0 || n == ret { return ret }

// STW !!! stopTheWorld(“GOMAXPROCS”)

newprocs = int32(n)

// 调用 procresize,并激活有任务的 P startTheWorld()

return ret }